{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Introducing tf.estimator\n",
    "\n",
    "**Learning Objectives**\n",
    "  - Understand where the `tf.estimator` module sits in the hierarchy of Tensorflow APIs\n",
    "  - Understand the workflow of creating a `tf.estimator` model\n",
    "    1. Create Feature Columns\n",
    "    2. Create Input Functions\n",
    "    3. Create Estimator\n",
    "    4. Train/Evaluate/Predict\n",
    "  - Understand how to swap in/out different types of Estimators\n",
    "  \n",
    "## Introduction\n",
    "\n",
    "Tensorflow is a hierarchical framework. The further down the hierarchy you go, the more flexibility you have, but that more code you have to write. Generally one starts at the highest level of abstraction. Then if you need additional flexibility drop down one layer.\n",
    "\n",
    "<img src='assets/TFHierarchy.png' width='50%'>\n",
    "<sup>(image: https://www.tensorflow.org/guide/premade_estimators)</sup>\n",
    "\n",
    "In this notebook we will be operating at the highest level of Tensorflow abstraction, using the Estimator API to predict taxifare prices on the sampled dataset we created previously."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import tensorflow as tf\n",
    "import pandas as pd\n",
    "import shutil\n",
    "\n",
    "print(tf.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Load raw data \n",
    "\n",
    "First, let's download the raw .csv data. These are the same files created in the `create_datasets.ipynb` notebook"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!gcloud storage cp gs://cloud-training-demos/taxifare/small/*.csv .\n",
    "!ls -l *.csv"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Because the files are small we can load them into in-memory Pandas dataframes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train = pd.read_csv(filepath_or_buffer = \"./taxi-train.csv\")\n",
    "df_valid = pd.read_csv(filepath_or_buffer = \"./taxi-valid.csv\")\n",
    "df_test = pd.read_csv(filepath_or_buffer = \"./taxi-test.csv\")\n",
    "\n",
    "CSV_COLUMN_NAMES = list(df_train)\n",
    "print(CSV_COLUMN_NAMES)\n",
    "\n",
    "FEATURE_NAMES = CSV_COLUMN_NAMES[1:] # all but first column\n",
    "LABEL_NAME = CSV_COLUMN_NAMES[0] # first column"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Create feature columns\n",
    "\n",
    "Feature columns make it easy to perform common type of feature engineering on your raw data. For example you can one-hot encode categorical data, create feature crosses, embeddings and more. We'll cover these later in the course, but if you want to a sneak peak browse the official TensorFlow [feature columns guide](https://www.tensorflow.org/guide/feature_columns).\n",
    "\n",
    "In our case we won't do any feature engineering. However we still need to create a list of feature columns because the Estimator we will use requires one. To specify the numeric values should be passed on without modification we use `tf.feature_column.numeric_column()`\n",
    "\n",
    "We use a [python list comprehension](https://www.pythonforbeginners.com/basics/list-comprehensions-in-python) to create the list of feature columns, which is just an elegant alternative to a for loop."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_columns = [tf.feature_column.numeric_column(key = k) for k in FEATURE_NAMES]\n",
    "feature_columns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Define input function\n",
    "\n",
    "Now that your estimator knows what type of data to expect and how to intepret it, you need to actually pass the data to it! This is the job of the input function.\n",
    "\n",
    "The input function returns a new batch of (features, label) tuples each time it is called by the Estimator.\n",
    "\n",
    "- features: A python dictionary. Each key is a feature column name and its value is the tensor containing the data for that feature\n",
    "- label: A Tensor containing the labels\n",
    "\n",
    "So how do we get from our current Pandas dataframes to (features, label) tuples that return one batch at a time?\n",
    "\n",
    "The `tf.data` module contains a collection of classes that allows you to easily load data, manipulate it, and pipe it into your model. https://www.tensorflow.org/guide/datasets_for_estimators"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def train_input_fn(df, batch_size = 128):\n",
    "    #1. Convert dataframe into correct (features,label) format for Estimator API\n",
    "    dataset = tf.data.Dataset.from_tensor_slices(tensors = (dict(df[FEATURE_NAMES]), df[LABEL_NAME]))\n",
    "    \n",
    "    # Note:\n",
    "    # If we returned now, the Dataset would iterate over the data once  \n",
    "    # in a fixed order, and only produce a single element at a time.\n",
    "    \n",
    "    #2. Shuffle, repeat, and batch the examples.\n",
    "    dataset = dataset.shuffle(buffer_size = 1000).repeat(count = None).batch(batch_size = batch_size)\n",
    "   \n",
    "    return dataset\n",
    "\n",
    "def eval_input_fn(df, batch_size = 128):\n",
    "    #1. Convert dataframe into correct (features,label) format for Estimator API\n",
    "    dataset = tf.data.Dataset.from_tensor_slices(tensors = (dict(df[FEATURE_NAMES]), df[LABEL_NAME]))\n",
    "    \n",
    "    #2.Batch the examples.\n",
    "    dataset = dataset.batch(batch_size = batch_size)\n",
    "   \n",
    "    return dataset\n",
    "\n",
    "def predict_input_fn(df, batch_size = 128):\n",
    "    #1. Convert dataframe into correct (features) format for Estimator API\n",
    "    dataset = tf.data.Dataset.from_tensor_slices(tensors = dict(df[FEATURE_NAMES])) # no label\n",
    "\n",
    "    #2.Batch the examples.\n",
    "    dataset = dataset.batch(batch_size = batch_size)\n",
    "   \n",
    "    return dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Choose Estimator\n",
    "\n",
    "Tensorflow has several premade estimators for you to choose from:\n",
    "\n",
    "- LinearClassifier/Regressor\n",
    "- BoostedTreesClassifier/Regressor\n",
    "- DNNClassifier/Regressor\n",
    "- DNNLinearCombinedClassifier/Regressor\n",
    "\n",
    "If none of these meet your needs you can implement a custom estimator using `tf.Keras`. We'll cover that later in the course.\n",
    "\n",
    "For now we will use the premade LinearRegressor. To instantiate an estimator simply pass it what feature columns to expect and specify an directory for it to output checkpoint files to."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "OUTDIR = \"taxi_trained\"\n",
    "\n",
    "model = tf.estimator.LinearRegressor(\n",
    "    feature_columns = feature_columns,\n",
    "    model_dir = OUTDIR,\n",
    "    config = tf.estimator.RunConfig(tf_random_seed = 1) # for reproducibility\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Train\n",
    "\n",
    "Simply invoke the estimator's `train()` function. Specify the `input_fn` which tells it how to load in data, and specify the number of steps to train for.\n",
    "\n",
    "By default estimators check the output directory for checkpoint files before beginning training, so it can pickup where it last left off. To prevent this we'll delete the output directory before starting training each time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "tf.logging.set_verbosity(tf.logging.INFO) # so loss is printed during training\n",
    "shutil.rmtree(path = OUTDIR, ignore_errors = True) # start fresh each time\n",
    "\n",
    "model.train(\n",
    "    input_fn = lambda: train_input_fn(df = df_train), \n",
    "    steps = 500)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Evaluate\n",
    "\n",
    "Estimators similarly have an `evaluate()` function. In this case we don't need to specify the number of steps to train because we didn't tell our input function to repeat the data. Once the input function reaches the end of the data evaluation will end. \n",
    "\n",
    "Loss is reported as MSE by default so we take the square root before printing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def print_rmse(model, df):\n",
    "    metrics = model.evaluate(input_fn = lambda: eval_input_fn(df))\n",
    "    print(\"RMSE on dataset = {}\".format(metrics[\"average_loss\"]**.5))\n",
    "print_rmse(model = model, df = df_valid)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "RMSE of 9.43 is  worse than our rules based benchmark (RMSE of $7.70). However given that we haven't done any feature engineering or hyperparameter tuning, and we're training on a small dataset using a simple linear model, we shouldn't yet expect good performance. \n",
    "\n",
    "The goal at this point is to demonstrate the mechanics of the Estimator API. In subsequent notebooks we'll improve on the model."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Predict\n",
    "\n",
    "To make predictions with our trained model, we call the `predict` method of our estimator, passing to it the input function to read in the data. Here we make predictions for the first 10 elements of the `df_test` dataframe. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "predictions = model.predict(input_fn = lambda: predict_input_fn(df = df_test[:10]))\n",
    "for items in predictions:\n",
    "    print(items)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Further evidence of the primitiveness of our model, it predicts similar amounts for every trip!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Change Estimator type\n",
    "\n",
    "One of the payoffs for using the Estimator API is we can swap in a different model type with just a few lines of code. Let's try a DNN. Note how now we need to specify the number of neurons in each hidden layer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "\n",
    "tf.logging.set_verbosity(tf.logging.INFO)\n",
    "shutil.rmtree(path = OUTDIR, ignore_errors = True) \n",
    "model = tf.estimator.DNNRegressor(\n",
    "    hidden_units = [10,10], # specify neural architecture\n",
    "    feature_columns = feature_columns, \n",
    "    model_dir = OUTDIR,\n",
    "    config = tf.estimator.RunConfig(tf_random_seed = 1)\n",
    ")\n",
    "model.train(\n",
    "    input_fn = lambda: train_input_fn(df = df_train), \n",
    "    steps = 500)\n",
    "print_rmse(model = model, df = df_valid)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our performance is only slightly better at 9.26, and still far worse than our rules based model.  This illustrates an important tenant of machine learning: A more complex model can't outrun bad data. \n",
    "\n",
    "Currently since we're not doing any feature engineering our input data has very little signal to learn from, so using a DNN doesn't help much."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Results summary\n",
    "\n",
    "We can summarize our results in a table here.\n",
    "   \n",
    "|Model | RMSE on validation set|\n",
    "|------|-----------------|\n",
    "|Rules Based Benchmark| 7.76|\n",
    "|Linear Model| 9.43|\n",
    "|DNN Model|9.26"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Challenge exercise\n",
    "\n",
    "Create a neural network that is capable of finding the volume of a cylinder given the radius of its base (r) and its height (h). Assume that the radius and height of the cylinder are both in the range 0.5 to 2.0. Simulate the necessary training dataset.\n",
    "<p>\n",
    "Hint (highlight to see):\n",
    "<p style='color:white'>\n",
    "The input features will be r and h and the label will be $\\pi r^2 h$\n",
    "Create random values for r and h and compute V.\n",
    "Your dataset will consist of r, h and V.\n",
    "Then, use a DNN regressor.\n",
    "Make sure to generate enough data.\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Copyright 2019 Google Inc. Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
